Still TO DO:

  • Extract repetitive plots and theory details for each part’s from this material –> intro slides

    • more interesting heatmaps for transformations section - all samples and allow seriation

    • complete intro slides for each part ABCD

  • Check length of material - assign elements as ‘optional extensions’ as needed

  • Make version without ‘answers’ - and knit as notebook without output

  • Clean up github repo.

  • microViz bug fixes if there’s time e.g.

    • bad default ranks on taxatree_plots

    • noisy new taxatree_plot_labels filter deprecation warnings

    • shiny warnings since 4.2 - requires dev version of shiny? or setting hidden option for strictness?

Setup

Planning

Timing:

5 hours (excluding 2x30 min coffee breaks)

  • 4 blocks of 1 hour & 15 mins

  • Typically: 15 minutes talking and 1 hour of hands-on

Part A:

  • 20-30 min intro: me - microViz dev and PhD; the dataset(s); & our aims - with pics
  • DADA2 denoising: Adapted the 2020 tutorial to my style and streamline/compress it.

Part B:

  • Intro to phyloseq and microViz
  • Bar plots & alpha diversity

Part C:

  • Distance and dissimilarity measures
  • Taxon filtering (prev. & abund.)
  • Ordination plots and PERMANOVA

Part D:

  • Taxa transformation and PCA (log and CLR)
  • Statistics with individual taxa - (simple) “differential abundance testing” and tree plots

SETUP

library(seriation)
library(dplyr)
library(purrr)
library(ggplot2)
library(phyloseq)
library(microViz)
library(shiny)
mice <- readRDS(here::here('data/mice.rds'))
load(here::here('data/shao19.rda'))
shao19 <- tax_fix(shao19, verbose = FALSE) %>% tax_names2rank("species")

We’ll use two datasets today, both are about the gut microbiome. The first is from a study of antibiotic administration in mice, and the second is from a large birth cohort study in human infants.

Each dataset was generated using different sequencing techniques, and different sequence data processing approaches. But the analyses approaches we will learn today can be readily applied to both methods. (The approaches are also appropriate for data other than the gut microbiota! but there is a lot of example gut microbiota data out there.)

  • mice is 16S rRNA gene amplicon sequencing data, from the mouse antibiotics study

    • They used Illumina MiSeq and processed the data into ASVs using DADA2
  • shao19 is shotgun metagenomic data, from a 2019 study of infants born vaginally or by C-section.

    • They used Illumina HiSeq and the abundance of species-like features was inferred with metaphlan3.

So the data has already been processed from fastq files into counts per taxon. This is the exciting bit, you get to explore and visualize the data, and do statistics (yay!).

Intro to phyloseq

This is a phyloseq S4 object, containing processed microbiota data from the mouse study.

Click here for more details about the mice dataset:

The mice data

The data originate from a study on the effects of oral antibiotic administration on flavivirus infection (https://www.ncbi.nlm.nih.gov/pubmed/29590614). Sequence data was generated from extracted nucleic acid from stool samples collected from individually caged mice and amplified using primers specific for the V4 region using primers 515F/806R.

The study followed flavivirus infection after the following treatments:

  1. Koolaid: Antibiotics are provided to the mice via their drinking water. As many of the antibiotics taste bad, koolaid is added as a sweetener Therefore, the appropriate control is water spiked and labelled koolaid.
  2. Ampicillin (Amp): https://en.wikipedia.org/wiki/Ampicillin
  3. Metronidazole (Met): https://en.wikipedia.org/wiki/Metronidazole
  4. Ampicillin + Metronidazole (Amp+Metro)

Treatments were supplied ad libitum for 2 weeks prior to viral infection and maintained for 2 weeks post-infection. Primary outcome was mouse survival. Each treatment group had two subgroups of mice that were either a) left uninfected as controls or b) infected with West Nile Virus via a subcutaneous foot pad injection.

Get a little familiar with the object. What does it have in it? Can you look at each part?

The printed object shows you functions you can use to access the data inside.

You can also use the @ symbol.

mice
## phyloseq-class experiment-level object
## otu_table()   OTU Table:         [ 3229 taxa and 520 samples ]
## sample_data() Sample Data:       [ 520 samples by 11 sample variables ]
## tax_table()   Taxonomy Table:    [ 3229 taxa by 7 taxonomic ranks ]
# View(mice)
tax_table(mice) %>% head()
## Taxonomy Table:     [6 taxa by 7 taxonomic ranks]:
##         Kingdom    Phylum           Class                 Order            
## ASV0001 "Bacteria" "Bacteroidetes"  "Bacteroidia"         "Bacteroidales"  
## ASV0002 "Bacteria" "Proteobacteria" "Gammaproteobacteria" "Xanthomonadales"
## ASV0003 "Bacteria" "Bacteroidetes"  "Bacteroidia"         "Bacteroidales"  
## ASV0004 "Bacteria" "Bacteroidetes"  "Bacteroidia"         "Bacteroidales"  
## ASV0005 "Bacteria" "Bacteroidetes"  "Bacteroidia"         "Bacteroidales"  
## ASV0006 "Bacteria" "Bacteroidetes"  "Bacteroidia"         "Bacteroidales"  
##         Family               Genus              Species      
## ASV0001 "Porphyromonadaceae" NA                 NA           
## ASV0002 "Xanthomonadaceae"   "Stenotrophomonas" "maltophilia"
## ASV0003 "Porphyromonadaceae" NA                 NA           
## ASV0004 "Porphyromonadaceae" NA                 NA           
## ASV0005 "Porphyromonadaceae" NA                 NA           
## ASV0006 "Porphyromonadaceae" NA                 NA
rank_names(mice)
## [1] "Kingdom" "Phylum"  "Class"   "Order"   "Family"  "Genus"   "Species"
otu_table(mice)[1:15, 1:8] 
## OTU Table:          [8 taxa and 15 samples]
##                      taxa are columns
##        ASV0001 ASV0002 ASV0003 ASV0004 ASV0005 ASV0006 ASV0007 ASV0008
## D14.A1    3343       0    4205    3470    3607    1210    1159    1852
## D14.B5    4332       0    5412    2494    3083    1451    1663    1745
## D0.D5     5344       0    3906    1439    2396    1402    1217    2078
## D0.E1     2994       0    4005    2188    2882    1267    1821    1788
## D0.E2     2315       0    3987     955    1665    1025     899    1342
## D0.E3     1972       0    4336    1876    3422    1208     551    1900
## D0.E4     2352       0    2561    1960    2060    1350    1095    1200
## D0.E5     1386       0    1752    1471    1363     724    1050     871
## D0.F1     3795       0    4052    2226    3130     539     162     247
## D0.F2     2867       0    2063     435    1412     410       0     150
## D0.F3     3850       0    4203     508    2839     533       0     230
## D0.F4     6740       0    5045    1356    5755     517      85     210
## D14.C1    2664       3    2892    1530    2176     735    1508    1577
## D0.F5     5303       3    4203    3313    4471    1241     344     441
## D0.G1     3052       0    3393    3209    2867     873     372    1512
# mice@otu_table[1:15, 1:10] # the same result
sample_variables(mice)
##  [1] "sample_id"       "barcode"         "run"             "plate"          
##  [5] "sample"          "sex"             "cage"            "treatment_days" 
##  [9] "treatment"       "virus"           "survival_status"
sample_data(mice)[1:15, 1:5]
##                 sample_id      barcode run plate sample
## D14.A1  1.Thackray.D14.A1 ACGAGACTGATT 368     1      1
## D14.B5 10.Thackray.D14.B5 ACCGGTATGTAC 368     1     10
## D0.D5  100.Thackray.D0.D5 ATCTACCGAAGC 368     2    100
## D0.E1  101.Thackray.D0.E1 ACTTGGTGTAAG 368     2    101
## D0.E2  102.Thackray.D0.E2 TCTTGGAGGTCA 368     2    102
## D0.E3  103.Thackray.D0.E3 TCACCTCCTTGT 368     2    103
## D0.E4  104.Thackray.D0.E4 GCACACCTGATA 368     2    104
## D0.E5  105.Thackray.D0.E5 GCGACAATTACA 368     2    105
## D0.F1  106.Thackray.D0.F1 TCATGCTCCATT 368     2    106
## D0.F2  107.Thackray.D0.F2 AGCTGTCAAGCT 368     2    107
## D0.F3  108.Thackray.D0.F3 GAGAGCAACAGA 368     2    108
## D0.F4  109.Thackray.D0.F4 TACTCGGGAACT 368     2    109
## D14.C1 11.Thackray.D14.C1 AATTGTGTCGGA 368     1     11
## D0.F5  110.Thackray.D0.F5 CGTGCTTAGGCT 368     2    110
## D0.G1  111.Thackray.D0.G1 TACCGAAGGTAT 368     2    111
sample_names(mice) %>% head(10) 
##  [1] "D14.A1" "D14.B5" "D0.D5"  "D0.E1"  "D0.E2"  "D0.E3"  "D0.E4"  "D0.E5" 
##  [9] "D0.F1"  "D0.F2"

Looking at microbiome data

Okay, so how do we look at the microbiota abundance data? To do this, we’re going to use the R package microViz

Lets take a very small subset of this data to get started, just the control group at day 13.

# We can filter the samples like this, using the sample_data information
mice %>% 
  ps_filter(treatment_days == 'D13', virus == 'WNV2000', treatment == 'Vehicle')
## phyloseq-class experiment-level object
## otu_table()   OTU Table:         [ 563 taxa and 10 samples ]
## sample_data() Sample Data:       [ 10 samples by 11 sample variables ]
## tax_table()   Taxonomy Table:    [ 563 taxa by 7 taxonomic ranks ]
mice %>% 
  ps_filter(treatment_days == 'D13', virus == 'WNV2000', treatment == 'Vehicle') %>%
  comp_barplot(
    tax_level = 'unique', n_taxa = 12, bar_width = 0.7,
    sample_order = 'asis', tax_transform_for_plot = 'identity'
  ) +
  coord_flip()

Rats. What is this junk? The unique taxa have uninformative IDs, and we also got a message about problems with the taxonomy table.

The total number of reads also varies a lot between samples! The total number of reads for each sample is NOT a reliable indicator of the biomass or bacterial load of each sample. So for now we will just consider the relative abundance of each taxon, as proportions of the total counts for that sample.

mice %>% 
  ps_filter(treatment_days == 'D13', virus == 'WNV2000', treatment == 'Vehicle') %>%
  comp_barplot(
    tax_level = 'unique', n_taxa = 12, sample_order = 'asis', bar_width = 0.7
  ) +
  coord_flip()
## NAs detected in phyloseq tax_table:
## Consider using tax_fix() to make taxa uniquely identifiable
## NAs detected in phyloseq tax_table:
## Consider using tax_fix() to make taxa uniquely identifiable

Let’s look at the taxonomy table first.

# tax_fix_interactive(mice) # run this in the R Console for an interactive look

Looks like we just need to fill in some blank cells when a sequence was not classified at genus or family. tax_fix can do this, it just copies down info from a higher rank classification. Let’s update our mice phyloseq object with this fix.

mice <- tax_fix(mice, verbose = FALSE)

We can also rename the unique taxa with a more informative name, according to their classification at Family level, and how common they are.

mice %>% taxa_names() %>% head
## [1] "ASV0001" "ASV0002" "ASV0003" "ASV0004" "ASV0005" "ASV0006"
mice <- tax_rename(mice, rank = 'Family')
mice %>% taxa_names() %>% head
## [1] "Porphyromonadaceae 001" "Xanthomonadaceae 01"    "Porphyromonadaceae 002"
## [4] "Porphyromonadaceae 003" "Porphyromonadaceae 004" "Porphyromonadaceae 005"

Let’s try again with the better names.

mice %>% 
  ps_filter(treatment_days == 'D13', virus == 'WNV2000', treatment == 'Vehicle') %>%
  comp_barplot(
    tax_level = 'unique', n_taxa = 12, sample_order = 'asis', bar_width = 0.7
    ) +
  coord_flip()

Sadly we don’t have enough distinct colours to show all the unique taxa.

mice %>% 
  ps_filter(treatment_days == 'D13', virus == 'WNV2000', treatment == 'Vehicle') %>%
  comp_barplot(
    tax_level = 'unique', n_taxa = 12, sample_order = 'asis', bar_width = 0.7,
    merge_other = FALSE
    ) +
  coord_flip()

So let’s “aggregate” all the counts into family-level groups.

mice %>% 
  ps_filter(treatment_days == 'D13', virus == 'WNV2000', treatment == 'Vehicle') %>%
  comp_barplot(
    tax_level = "Family", n_taxa = 10, bar_width = 0.7, sample_order = 'asis'
  ) +
  coord_flip()

By aggregating at family level, we have sacrificed taxonomic resolution, compared to using ASVs. But this way we can get an idea of which families are the most abundant, and how variable the communities are.

Try making some similar plots aggregated at different taxonomic ranks.

Change the tax_level argument.

# mice %>%
#   ps_filter(treatment_days == 'D13', virus == 'WNV2000', treatment == 'Vehicle') %>%
#   comp_barplot(tax_level = , n_taxa = 10, sample_order = 'asis', merge_other = FALSE)
mice %>% 
  ps_filter(treatment_days == 'D13', virus == 'WNV2000', treatment == 'Vehicle') %>% 
  comp_barplot(
    tax_level = "Genus", n_taxa = 12, bar_width = 0.7,
    sample_order = 'asis', merge_other = FALSE
  ) +
  coord_flip()

Many of the ASVs in this mice data, the Porphyromonadaceae, could not be classified at genus level.

mice %>% 
  ps_filter(treatment_days == 'D13', virus == 'WNV2000', treatment == 'Vehicle') %>% 
  comp_barplot(
    tax_level = "Phylum", n_taxa = 7, bar_width = 0.7, sample_order = 'asis'
  ) +
  coord_flip()

A note on phylum names! There have been major changes this year and some of these are now old names. Most published research is of course with the old names (and still probably will be for a year or so).

INSERT NAME CONVERSION TABLE.

Alpha diversity

How diverse is the bacterial microbiome of each sample?

Why is this interesting?

Biologically

  • Lower gut microbiome diversity is related to worse health in adult humans.
  • Higher diversity ecosystems are often considered healthier, more mature, and more resilient to perturbation.
  • BUT: diverse == healthy does not hold for all ecosystems, e.g. early infant gut microbiome, so consider your own data and hypotheses carefully.

Practically

Simple one number summary of ecosystem, easy to compare and do stats with.

Richness

The more the merrier. The simplest measure is just counting, aka “Observed Richness”. Let’s compute the observed richness and label each sample with it.

mice %>% 
  ps_filter(treatment_days == 'D13', virus == 'WNV2000', treatment == 'Vehicle') %>% 
  ps_calc_richness(rank = 'Genus', index = 'observed', varname = 'N genera') %>%
  comp_barplot(
    tax_level = "Genus", n_taxa = 12, label = 'N genera', bar_width = 0.7,
    sample_order = 'asis', merge_other = FALSE#, tax_transform_for_plot = 'identity'
  ) +
  coord_flip()

Diversity

Richness and evenness matter. Shannon index, TODO: describe Shannon index and exp shannon.

mice %>% 
  ps_filter(treatment_days == 'D13', virus == 'WNV2000', treatment == 'Vehicle') %>% 
  ps_calc_diversity(rank = 'Genus', index = 'shannon') %>%
  ps_mutate(shannon_Genus = round(shannon_Genus, digits = 2)) %>% 
  comp_barplot(
    tax_level = "Genus", n_taxa = 12, label = 'shannon_Genus', bar_width = 0.7,
    sample_order = 'asis', merge_other = FALSE
  ) +
  coord_flip()

Statistics with alpha diversity

So we have our alpha diversity values for this small subset of mice.

mice %>% 
  ps_filter(treatment_days == 'D13', virus == 'WNV2000', treatment == 'Vehicle') %>% 
  ps_calc_diversity(rank = 'Genus', index = 'shannon') %>%
  samdat_tbl() %>% 
  ggplot(aes(x = shannon_Genus, y = "Day 13\ncontrols")) + 
  geom_point(position = position_jitter(height = 0.2), alpha = 0.5) +
  labs(x = 'Shannon diversity (Genus)', y = NULL) +
  xlim(1, 2.5) +
  theme_bw() 

Let’s calculate alpha diversity for all mice after antibiotic or control treatment, and make a comparison. I suspect that the average gut microbiota diversity of the antibiotic exposed mice will differ from the control group’s at day 3.

# First compute a new variable aggregating all the control mice together
mice <- mice %>% 
  ps_mutate(antibiotics = treatment %in% c("Amp", "Metro", "AmpMetro"))
mice %>% 
  ps_filter(treatment_days == 'D13') %>% 
  ps_calc_diversity(rank = 'Genus', index = 'shannon') %>%
  samdat_tbl() %>% 
  ggplot(aes(y = antibiotics, x = shannon_Genus)) +
  geom_boxplot(width = 0.3) + 
  geom_point(position = position_jitter(height = 0.2), alpha = 0.5) +
  theme_bw()

It looks like the antibiotics treated mice have lower gut microbiota diversity on average. A simple statistical test supports this.

mice %>% 
  ps_filter(treatment_days == 'D13') %>% 
  ps_calc_diversity(rank = 'Genus', index = 'shannon') %>%
  samdat_tbl() %>% 
  wilcox.test(formula = shannon_Genus ~ antibiotics, data = .)
## 
##  Wilcoxon rank sum test with continuity correction
## 
## data:  shannon_Genus by antibiotics
## W = 533, p-value = 0.0142
## alternative hypothesis: true location shift is not equal to 0

You can apply more complex statistical tests as you like, e.g. adjusting for covariates with linear regression, using lm()

mice %>% 
  ps_filter(treatment_days == 'D13') %>% 
  ps_calc_diversity(rank = 'Genus', index = 'shannon') %>%
  samdat_tbl() %>% 
  lm(formula = shannon_Genus ~ antibiotics + virus, data = .) %>% 
  summary()
## 
## Call:
## lm(formula = shannon_Genus ~ antibiotics + virus, data = .)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -1.34105 -0.20830  0.08928  0.48767  1.25856 
## 
## Coefficients:
##                 Estimate Std. Error t value Pr(>|t|)    
## (Intercept)       1.5947     0.2036   7.831 7.76e-11 ***
## antibioticsTRUE  -0.5445     0.1908  -2.854  0.00587 ** 
## virusWNV2000      0.3217     0.1742   1.847  0.06956 .  
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.6479 on 62 degrees of freedom
## Multiple R-squared:  0.1535, Adjusted R-squared:  0.1262 
## F-statistic: 5.622 on 2 and 62 DF,  p-value: 0.005706

Try it out for yourself at other time points? Practice making plots and doing simple statistical tests.

What about richness?

Click here for an extension activity with an IBD dataset:

This is an extension exercise, for those who are moving quickly.

Inflammatory Bowel Disease study

ibd <- corncob::ibd_phylo %>% 
  tax_mutate(Species = NULL) %>% # ibd_phylo Species column was blank -> deleted
  ps_mutate(disease = ibd == 'ibd', ibd = NULL) # adds disease state indicator variable

ibd is another phyloseq object containing 16S rRNA gene amplicon sequencing data, from a 2012 study of Inflammatory Bowel Disease in children and young adults.

It’s “old” data: they used 454 Pyrosequencing, and clustered the raw sequences into “OTUs”.

Have a look at the data, like we did before for the mice dataset.

ibd
## phyloseq-class experiment-level object
## otu_table()   OTU Table:         [ 36349 taxa and 91 samples ]
## sample_data() Sample Data:       [ 91 samples by 15 sample variables ]
## tax_table()   Taxonomy Table:    [ 36349 taxa by 6 taxonomic ranks ]

You can perform alpha diversity analysis: Try comparing the alpha diversity of the IBD patients against the healthy controls.

Click here for additional notes on richness and readcount

Additional notes on richness and readcount

Simple approaches like Observed Richness are sensitive to what ecologists call “sampling effort”. For macroecologists, this is actually how much time/effort you spent trying to count all the organisms present in an ecosystem. In our case, the amount of total reads obtained represents the sampling effort: more reads, more effort. Indeed we can see that the samples with a much lower readcount have lower observed richness.

(Furthermore, as this richness estimate is based on a sample, and not the actual ecosystem, the richness estimate actually has quantifiable uncertainty too.)

mice %>% 
  ps_filter(treatment_days == 'D13', virus == 'WNV2000', treatment == 'Amp') %>%
  ps_calc_richness(rank = 'Genus', index = 'observed', varname = 'N genera') %>%
  comp_barplot(
    tax_level = "Genus", n_taxa = 12, label = 'N genera', bar_width = 0.7,
    sample_order = 'asis', merge_other = FALSE, tax_transform_for_plot = 'identity'
  )

mice %>% 
  ps_calc_richness(rank = 'Genus', index = 'observed', varname = 'genera') %>%
  ps_mutate(readcount = sample_sums(mice)) %>% 
  samdat_tbl() %>% 
  ggplot(aes(readcount, genera)) + 
  geom_point(alpha = 0.4, size = 2.5) +
  theme_bw(14)

What to do:

  1. Simple solution: Ignore the problem. Whilst you can’t interpret the richness of any individual sample as being correct, it is still usually valid to compare richness across groups of samples, as the readcount variation is only random noise, and should be uncorrelated with your grouping variable (but do check this).
  2. Harder solution: Explore more rigorous methods like breakaway by Amy Willis and team. TODO: INSERT LINK.

DINNER BREAK


Beta diversity (part 1)

TODO - topic intro slides

So far, this has been relatively straightforward, at least conceptually. But there is a lot more interesting stuff we can do with microbiome data.

We’ve looked at one sample at a time and calculated and compared simple summary measures of sample alpha-diversity.

Alpha diversity is sometimes referred to as “within sample” diversity.

Now we’re going to look at “beta diversity”, or “between sample” diversity.

For this part we’re going to swap to another dataset. So you get a little bit more practice examining a phyloseq object. Look at the rank names, sample data variables etc.

shao19 # this object has another part!
## phyloseq-class experiment-level object
## otu_table()   OTU Table:         [ 819 taxa and 1644 samples ]
## sample_data() Sample Data:       [ 1644 samples by 11 sample variables ]
## tax_table()   Taxonomy Table:    [ 819 taxa by 6 taxonomic ranks ]
## phy_tree()    Phylogenetic Tree: [ 819 tips and 818 internal nodes ]
#

Filtering

First, we need to talk about filtering.

Sample filtering

You should check if any of your samples have a surprisingly low total number of (classified) reads. This can suggest that something went wrong in the lab (or during sample collection) and the data from this sample might be unreliable.

You might already do this check for total reads and remove poor quality samples during the fastq file processing.

shao19 %>% 
  ps_mutate(reads = sample_sums(shao19)) %>% 
  samdat_tbl() %>% 
  ggplot(aes(x = reads)) + 
  geom_freqpoly(bins = 500) +
  geom_rug(alpha = 0.5) +
  scale_x_log10(labels = scales::label_number()) +
  labs(x = 'Number of classified reads', y = NULL) +
  theme_bw()

How many is enough? There is no easy answer.

These samples have great depth. There are a few with much less reads than the rest, and a few with under a million. You might consider dropping the samples with under a million reads, to see if it affects your results, but in this case we won’t.

But 100,000 is still a lot, compared to what older sequencing machines produced: 1000 reads might have been considered very good. So look at the distribution for your data, in case there are obvious outliers, and look at recent papers using a similar sequencing technique for what kind of threshold they used.

There might also be relevant information for the type of sequencer you used on e.g. Illumina website. e.g. for this type of sequencing Illumina suggests you should expect at least a million reads (and this is good for RNA seq analyses). https://support.illumina.com/bulletins/2017/04/considerations-for-rna-seq-read-length-and-coverage-.html

If you are interested, go back and recreate this plot with the 16S sequencing dataset mice.

mice %>% 
  ps_mutate(reads = sample_sums(.env$mice)) %>% 
  samdat_tbl() %>% 
  ggplot(aes(x = reads)) + 
  geom_freqpoly(bins = 30) +
  geom_rug(alpha = 0.5) +
  scale_x_log10(labels = scales::label_number()) +
  labs(x = 'Number of classified reads', y = NULL) +
  theme_bw()

Taxon filtering

Okay, so we might remove “bad” samples, but how can a taxon be “bad”?

We probably want to filter out rare taxa, before performing some kinds of analysis.

Why remove rare taxa?

Rare taxa might be:

  1. Sequencing errors
  2. Statistically problematic (rare )
  3. Biologically irrelevant

More elaboration for presentation/discussion

  1. Rare taxa might be erroneous: sequencing errors, chimeric sequences…

  2. Statistical challenges:

    • There are often A LOT of different rare taxa
      • Sparsity, lots of zeros
      • Noise, more measurement error in very low abundance taxa
      • Multiple comparisons (we’ll get to that later)
    • Having many many taxa in your dataset can really slow down your analysis!

    TODO: COMPLETE

  3. Biological (ir)relevance:

    • For many (most?) microbiome research questions, rare taxa are less likely to be relevant
    • For common host health problems and the human/animal microbiome, if a taxon only occurs in every 100th person, it is less likely to be relevant to risk common disease outcomes (?)

I’m sure one can think of many cases where rare taxa are biologically relevant, but problems 1 and 2 are still there.

How to remove rare taxa?

What is rare? Two main concepts.

  • Low prevalence - taxon only detected in a small number of samples in your dataset.
  • Low abundance - relatively few reads assigned to that taxon (on average or in total)

Consider the impact of issues 1, 2, and 3. Let’s say we are not interested in unique taxa that occur in fewer than 2% of samples, and they have to have at least 10,000 reads in total across all samples.

# before filtering
ntaxa(shao19)
## [1] 819
# after filtering
shao19 %>% 
  tax_filter(min_prevalence = 2 / 100, min_total_abundance = 10000) %>% 
  ntaxa()
## Proportional min_prevalence given: 0.02 --> min 33/1644 samples.
## [1] 253

Wow so that would remove most of our unique taxa! What is going on? Let’s make some plots!

# make table of summary statistics for the unique taxa in shao19
shaoTaxaStats <- tibble(
  taxon = taxa_names(shao19),
  prevalence = microbiome::prevalence(shao19),
  total_abundance = taxa_sums(shao19)
)
p <- shaoTaxaStats %>% 
  ggplot(aes(total_abundance, prevalence)) + 
  geom_point(alpha = 0.5) +
  geom_rug(alpha = 0.1) +
  scale_x_continuous(labels = scales::label_number(), name = 'Total Abundance') + 
  scale_y_continuous(
    labels = scales::label_percent(), breaks = scales::breaks_pretty(n = 9),
    name = "Prevalence (%)",
    sec.axis = sec_axis(
      trans = ~ . * nsamples(shao19), breaks = scales::breaks_pretty(n = 9),
      name = "Prevalence (N samples)"
    )
  ) +
  theme_bw()
p

So most taxa have a low prevalence, and handful have way more reads than most.

Let’s quickly label those plots to check which taxa are the big time players.

p + ggrepel::geom_text_repel(
  data = function(df) filter(df, total_abundance > 1e9 | prevalence > 0.6),
  mapping = aes(label = taxon), size = 2.5, min.segment.length = 0, force = 15
)

That makes sense for this dataset of mostly infant gut microbiome samples.

Now let’s zoom in on the less abundant taxa by log-transforming the axes. We’ll also add lines indicating the thresholds of 2% prevalence and 10000 reads abundance.

shaoTaxaStats %>% 
  ggplot(aes(total_abundance, prevalence)) + 
  geom_vline(xintercept = 10000, color = 'red', linetype = 'dotted') +
  geom_hline(yintercept = 2/100, color = 'red', linetype = 'dotted') +
  geom_point(alpha = 0.5) +
  geom_rug(alpha = 0.1) +
  scale_x_log10(labels = scales::label_number(), name = 'Total Abundance') + 
  scale_y_log10(
    labels = scales::label_percent(), breaks = scales::breaks_log(n = 9),
    name = "Prevalence (%)",
    sec.axis = sec_axis(
      trans = ~ . * nsamples(shao19), breaks = scales::breaks_log(n = 9),
      name = "Prevalence (N samples)"
    )
  ) +
  theme_bw()

We can break this down by phylum if we add the taxonomic table information.

# don't worry about this code if it's confusing, just focus on the plot output
shao19 %>% 
  tax_table() %>% 
  as.data.frame() %>% 
  as_tibble(rownames = 'taxon') %>% 
  left_join(shaoTaxaStats, by = "taxon") %>% 
  add_count(phylum, name = 'phylum_count', sort = TRUE) %>% 
  mutate(phylum = factor(phylum, levels = unique(phylum))) %>% # to fix facet order
  mutate(phylum = forcats::fct_lump_n(phylum, n = 5) %>% forcats::fct_explicit_na('Other')) %>% 
  ggplot(aes(total_abundance, prevalence)) +
  geom_vline(xintercept = 10000, color = 'red', linetype = 'dotted') +
  geom_hline(yintercept = 2/100, color = 'red', linetype = 'dotted') +
  geom_point(alpha = 0.5, size = 1) +
  geom_rug(alpha = 0.2) +
  scale_x_log10(
    labels = scales::label_log(), breaks = scales::breaks_log(n = 5),
    name = 'Total Abundance'
  ) + 
  scale_y_log10(
    labels = scales::label_percent(), breaks = scales::breaks_log(n = 9),
    name = "Prevalence (%)",
    sec.axis = sec_axis(
      trans = ~ . * nsamples(shao19), breaks = scales::breaks_log(n = 9),
      name = "Prevalence (N samples)"
    )
  ) +
  facet_wrap('phylum') + 
  theme_bw(10)

How to pick a threshold?

Depends on what analysis method you are filtering for!

  • alpha diversity = DO NOT FILTER
  • beta diversity = relevance of threshold depends on your distance measure (next topic!)
  • differential abundance testing = stringent filtering, prevalence >5%, >10%? (last topic!)

Dissimilarity measures

TODO: Slides on different dissimilarity measures.

What are we doing? Calculating the dissimilarity of two samples’ compositions.

[Visualize distance heatmaps?]

  • Binary Jaccard
  • Bray-Curtis
  • UniFrac distances (unweighted, weighted, generalised)

To simplify and speed up the analyses, we’re going to take a smaller part of the dataset. We’ll only look at the 300 infant fecal samples from 4 days of age.

shao4 <- shao19 %>% ps_filter(family_role == 'child', infant_age == 4) 

We’re going to filter out rare taxa quite strictly, for similar reasons. But we won’t overwrite our smaller dataset: we’ll do the filtering per analysis.

shao4 %>% 
  tax_filter(min_prevalence = 2.5/100) %>% 
  tax_agg(rank = 'genus') %>% 
  tax_transform('binary') %>% # converts counts to absence/presence: 0/1
  dist_calc(dist = 'jaccard') 
## Proportional min_prevalence given: 0.025 --> min 8/306 samples.
## ps_extra object - a list with phyloseq and extras:
## 
## phyloseq-class experiment-level object
## otu_table()   OTU Table:         [ 35 taxa and 306 samples ]
## sample_data() Sample Data:       [ 306 samples by 11 sample variables ]
## tax_table()   Taxonomy Table:    [ 35 taxa by 5 taxonomic ranks ]
## 
## ps_extra info:
## tax_agg = genus tax_transform = binary
## 
## jaccard distance matrix of size 306 
## 0.6666667 0.7333333 0.9375 0.8125 0.6428571 ...
## 
## $counts OTU Table: [ 35 taxa and 306 samples ]

Talk about the output we see here. Then look at the distance matrix.

distances <- shao4 %>% 
  tax_filter(min_prevalence = 2.5/100, verbose = FALSE) %>% 
  tax_agg(rank = 'genus') %>% 
  tax_transform('binary') %>%
  dist_calc(dist = 'jaccard') %>% 
  dist_get() 

as.matrix(distances)[1:5, 1:5]
##             B01089_ba_4 B01190_ba_4 B01194_ba_4 B01196_ba_4 B01235_ba_4
## B01089_ba_4   0.0000000   0.6666667   0.7333333   0.9375000   0.8125000
## B01190_ba_4   0.6666667   0.0000000   0.7500000   0.9166667   0.8461538
## B01194_ba_4   0.7333333   0.7500000   0.0000000   0.4615385   0.3076923
## B01196_ba_4   0.9375000   0.9166667   0.4615385   0.0000000   0.4615385
## B01235_ba_4   0.8125000   0.8461538   0.3076923   0.4615385   0.0000000
range(distances)
## [1] 0 1

Ordination

What do we do with these distances or dissimilarities? We make an ordination.

Ordination refers to the process of ordering things (in our case: samples), so that similar things (samples) are closer to each other, and dissimilar things (samples) are further away.

PCoA

Principal Co-ordinates Analysis is one kind of ordination.

Takes distance matrix and finds new dimensions (a co-ordinate system if you like)

More details this works

shao4 %>% 
  tax_filter(min_prevalence = 2.5/100, verbose = FALSE) %>% 
  tax_agg(rank = 'genus') %>% 
  dist_calc(dist = 'bray') %>% 
  ord_calc(method = 'PCoA') %>% 
  ord_plot(alpha = 0.6, size = 2) +
  theme_classic(12) +
  coord_fixed(0.7)

To get a little insight into what the happened here, we can colour each sample according to its dominant (most abundant) genus.

shao4 %>% 
  tax_filter(min_prevalence = 2.5/100, verbose = FALSE) %>% 
  ps_calc_dominant(rank = 'genus', none = "Mixed", other = "Other") %>% 
  tax_agg(rank = 'genus') %>% 
  dist_calc(dist = 'bray') %>% 
  ord_calc(method = 'PCoA') %>% 
  ord_plot(color = "dominant_genus", alpha = 0.6, size = 2) +
  scale_color_brewer(name = "Dominant Genus", palette = 'Dark2') +
  theme_classic(12) +
  coord_fixed(0.7)

Interactive ordination!

microViz provides a Shiny app ord_explore to interactively create and explore PCoA plots and other ordinations. See the code below to get started.

Here are a few things to try:

  • Colour the samples using the variables in the sample data
  • Select a few samples to view their composition on barplots!
  • Change some ordination options:
    • Different rank of taxonomic aggregation
    • Different distances we’ve discussed
  • Copy the automatically generated code
    • Exit the app (press escape or click red button in R console!)
    • Paste and run the code to recreate the ordination plot
    • Customise the plot: change colour scheme, title, etc.
  • Launch the app again with a different subset of the data
    • Practice using ps_filter etc.
    • e.g. plot the data of the mothers’ gut microbiomes!
    • compute one or more alpha diversity measures

Beware:

  • UniFrac distances can be quite slow (over a minute) to calculate!
    • Filter to fewer samples and fewer taxa to speed it up (Before launching the app)
  • There are many distances available, feel free to try out ones we haven’t talked about
    • BUT:
      • You shouldn’t use a distance that you don’t understand in your actual work, even if the plot looks nice! ;)
      • Some of them might not work…
        • They are mostly implemented in the package vegan and I haven’t tested them all
        • Errors will appear in the RStudio R console
        • You can report to me any distances that don’t work if you’re feeling helpful!
  • There are other ordination methods available in ord_explore, which we haven’t discussed
    • We will discuss PCA and various transformations after dinner!
    • Some things we won’t have time to cover, but you can look here for info on topics like constrained ordination –> TODO: insert gusta me ecology website link
# fire up the shiny app
# run these lines in your console
shao4 %>% 
  tax_filter(min_prevalence = 2.5/100, verbose = FALSE) %>% 
  # calculate new sample variables with dominant taxon (optional)
  ps_calc_dominant(rank = 'genus', none = "Mixed", other = "Other") %>% 
  # launch a Shiny app in your web browser!
  ord_explore()
# different options
# run this line in your console
shao19 %>% 
  ps_filter(family_role == 'mother') %>% 
  tax_filter(min_prevalence = 2.5/100, verbose = FALSE) %>% 
  # calculate a few sample variables for interest (optional)
  ps_calc_dominant(rank = 'genus', none = "Mixed", other = "Other") %>% 
  ps_calc_diversity(rank = 'genus', index = 'shannon') %>% 
  ps_calc_richness(rank = 'genus', index = 'observed') %>% 
  # launch a Shiny app in your web browser!
  ord_explore()

PERMANOVA:

Permutational multivariate analysis of variance.

  • ANOVA - analysis of variance (statistical modelling approach)
  • Multivariate - more than one dependent variable (multiple taxa!)
  • Permutational - statistical significance estimates obtained by shuffling the data many times

(Sometimes also called NP-MANOVA, non-parametric MANOVA e.g. on gusta me - insert link TODO)

TLDR: Are those groups on the PCoA actually different??

shao4 %>% 
  tax_filter(min_prevalence = 2.5/100, verbose = FALSE) %>% 
  tax_agg(rank = 'genus') %>% 
  dist_calc(dist = 'bray') %>% 
  ord_calc(method = 'PCoA') %>% 
  ord_plot(alpha = 0.6, size = 2, color = 'birth_mode') +
  theme_classic(12) +
  coord_fixed(0.7) +
  stat_ellipse(aes(color = birth_mode)) +
  scale_color_brewer(palette = 'Set1')

shao4 %>% 
  tax_filter(min_prevalence = 2.5/100, verbose = FALSE) %>% 
  tax_agg(rank = 'genus') %>% 
  dist_calc(dist = 'bray') %>% 
  dist_permanova(variables = 'birth_mode', n_perms = 99, seed = 123) %>% 
  perm_get()
## 2022-05-24 06:10:01 - Starting PERMANOVA with 99 perms with 1 processes
## 2022-05-24 06:10:01 - Finished PERMANOVA
## Permutation test for adonis under NA model
## Marginal effects of terms
## Permutation: free
## Number of permutations: 99
## 
## vegan::adonis2(formula = formula, data = metadata, permutations = n_perms, by = by, parallel = parall)
##             Df SumOfSqs      R2      F Pr(>F)   
## birth_mode   1   13.790 0.12366 42.898   0.01 **
## Residual   304   97.727 0.87634                 
## Total      305  111.518 1.00000                 
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
# Use more permutations for a more reliable p.value in your real work (slower)
# Set a random seed number for reproducibility of this stochastic method

You can see from the model output that the p value, Pr(>F) is below 0.05. So there is good statistical evidence that the bacterial gut microbiota composition of c-section delivered infants has a different composition than vaginally delivered infants at 4 days of age.

You should also report that you used Bray-Curtis dissimilarities, calculated on genera. (after keeping only unique taxa with a prevalence of at least 2.5%!)

It’s probably a good idea to decide on a couple of appropriate distance measures up front for these tests, and report both (at least in supplementary material), as the choice of distance measure can affect results and conclusions!

You can also adjust for covariates in PERMANOVA, and often should. Let’s fit a more complex model, adjusting for infant sex, birth weight, and the total number of assigned reads.

shao4 %>% 
  tax_filter(min_prevalence = 2.5/100, verbose = FALSE) %>% 
  tax_agg(rank = 'genus') %>% 
  dist_calc(dist = 'bray') %>% 
  dist_permanova(
    variables = c('birth_mode', 'sex', 'birth_weight', 'number_reads'),
    n_perms = 99, seed = 111
  ) %>% 
  perm_get()
## Dropping samples with missings: 15
## 2022-05-24 06:10:01 - Starting PERMANOVA with 99 perms with 1 processes
## 2022-05-24 06:10:02 - Finished PERMANOVA
## Permutation test for adonis under reduced model
## Marginal effects of terms
## Permutation: free
## Number of permutations: 99
## 
## vegan::adonis2(formula = formula, data = metadata, permutations = n_perms, by = by, parallel = parall)
##               Df SumOfSqs      R2       F Pr(>F)   
## birth_mode     1   10.794 0.10163 34.8045   0.01 **
## sex            1    0.280 0.00264  0.9031   0.43   
## birth_weight   1    0.565 0.00532  1.8215   0.06 . 
## number_reads   1    2.873 0.02705  9.2656   0.01 **
## Residual     286   88.696 0.83509                  
## Total        290  106.211 1.00000                  
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
# Use more permutations for a more reliable p.value in your real work (slower)
# Set a random seed number for reproducibility of this stochastic method

COFFEE BREAK 2 (30 min)


Beta diversity (part 2)

Euclidean distances

What about Euclidean distances?

What are those? Slides

  • Show pythagoras \(c = \sqrt{a^2 + b^2}\) and explain generalisation to more dimensions, where each dimension is the counts of one taxon.

Issues

  • Sensitive to sparsity (double-zero problem) –> filter rare taxa
  • Excessive emphasis on high-abundance taxa –> transform features first
  • The PCoA looks weird! most samples bunched in the middle with spindly projections..
shao4 %>% 
  tax_filter(min_prevalence = 2.5/100, verbose = FALSE) %>% 
  tax_agg(rank = 'genus') %>% 
  dist_calc(dist = 'euclidean') %>% 
  ord_calc(method = 'PCoA') %>% 
  ord_plot(alpha = 0.6, size = 2) +
  theme_classic(12) +
  coord_fixed(0.7) + 
  geom_rug(alpha = 0.1)

Abundance transformation

We already did two transformations with tax_transform(): binary (for Binary Jaccard distances) and compositional (for barplots).

Now we need log transformations, and the centered-log-ratio, CLR, transformation.

Log transformation

First let’s look at the abundance again, this time with heatmaps.

# Getting the taxa in abundance order up front
# to keep it consistent across multiple plots
shao4_sorted <- shao4 %>% 
  tax_sort(by = sum, at = 'genus', trans = 'compositional', tree_warn = FALSE)

Each column is a sample (from an infant), and each row is a taxon.

shao4_sorted %>% 
  tax_transform(trans = 'identity', rank = 'genus') %>% 
  comp_heatmap(
    samples = 1:30, taxa = 1:20, grid_lwd = 2, name = 'Counts',
    tax_seriation = 'Identity', sample_seriation = "Identity"
  )

shao4_sorted %>% 
  tax_transform(trans = 'compositional', rank = 'genus') %>% 
  comp_heatmap(
    samples = 1:30, taxa = 1:20, grid_lwd = 2, name = 'Prop.',
    tax_seriation = 'Identity', sample_seriation = "Identity"
  )

Even though we have picked the top 20 most abundant genera, there are still a lot of zeros, We need to deal with the zeros, because log(0) is undefined. The solution is to add a small amount to every value (or just every zero), before applying the log transformation. This small value is often called a pseudo-count.

What value should we use for the pseudo-count?

One option is to just add 1, and another popular option is to add half of the smallest observed real value (from across the whole dataset).

shao4_sorted %>% 
  tax_transform(rank= 'genus', trans = 'log10', zero_replace = 1) %>% 
  comp_heatmap(
    samples = 1:30, taxa = 1:20, grid_lwd = 2, name = 'log10\n(x+1)', 
    tax_seriation = 'Identity', sample_seriation = "Identity"
  )

shao4_sorted %>% 
  tax_agg(rank = 'genus') %>% 
  # tax_transform(trans = 'compositional') %>% # compositional also possible
  tax_transform(trans = 'log10', zero_replace = 'halfmin', chain = TRUE) %>% 
  comp_heatmap(
    samples = 1:30, taxa = 1:20, grid_lwd = 2, name = 'log10\nhalfmin', 
    tax_seriation = 'Identity', sample_seriation = "Identity"
  )

Log-transformation, zero replacement summary (keep it simple and record your approach).

Centered log ratio transformation:

Compositionality - centered-log-ratio transformation

The centered log-ratio (clr) transformation uses the geometric mean of the sample vector as the reference

shao4_sorted %>% 
  tax_agg(rank = 'genus') %>% 
  # tax_transform(trans = 'compositional') %>% # compositional also possible
  tax_transform(trans = 'clr', zero_replace = 'halfmin', chain = TRUE) %>% 
  comp_heatmap(
    samples = 1:30, taxa = 1:20, grid_lwd = 2, name = 'CLR\nhalfmin', 
    colors = heat_palette(sym = TRUE),
    tax_seriation = 'Identity', sample_seriation = "Identity"
  )

Overview of CoDa problem - slides only.

The sequencing data gives us relative abundances, not absolute abundances. The total number of reads sequenced per sample is an arbitrary total.

If one taxon blooms, whilst everything else stays stable, the relative abundance of all other taxa must (appear to) go down.

This leads to two main types of problem:

  • interpretation caveats: see differential abundance section later
  • statistical issues: taxon abundances are not independent, but (weakly?) negatively correlated

This is worse with simpler ecosystems. There is the same problem in theory with RNAseq data, but I suspect it is less bothersome because there are many more competing “species” of RNA transcript than there are bacterial species in even a very complex microbiome.

The centered-log-ratio transformation (along with some other similar ratio transformations) are claimed to help with the statistical issues by transforming the abundances from the simplex to the real space.

Practically, the CLR transformation involves finding the geometric mean of each sample, and then dividing abundance of each taxon in that sample by this geometric mean. Finally you take the natural log of this ratio.

For more details, check out Gloor 2017, and work by Thomas Quinn. TODO: link.

PCA

Principal Components Analysis.

Quite similar to Principal Co-ordinates Analysis.

In fact, PCA produces equivalent results to PCoA with euclidean distances. So let’s perform the CLR-transform first and check PCA and euclidean PCoA are the same.

shao4 %>% 
  tax_filter(min_prevalence = 2.5/100, verbose = FALSE) %>% 
  tax_transform(rank = 'genus', trans = 'clr', zero_replace = 'halfmin') %>% 
  dist_calc(dist = 'euclidean') %>% 
  ord_calc(method = 'PCoA') %>% 
  ord_plot(alpha = 0.6, size = 2, color = 'birth_mode') +
  theme_classic(12) +
  coord_fixed(0.7) 

shao4 %>% 
  tax_filter(min_prevalence = 2.5/100, verbose = FALSE) %>% 
  tax_transform(rank = 'genus', trans = 'clr', zero_replace = 'halfmin') %>% 
  ord_calc(method = 'PCA') %>% 
  ord_plot(alpha = 0.6, size = 2, color = 'birth_mode') +
  theme_classic(12) +
  coord_fixed(0.7)

So why is PCA interesting for us? Because the Principal components are built directly from a (linear) combination of the original features.

That means we know how much each taxon contributes to each PC axis, and we can plot this information (loadings) as arrows, alongside the sample points.

pca <- shao4 %>% 
  tax_filter(min_prevalence = 2.5/100, verbose = FALSE) %>% 
  tax_transform(rank = 'genus', trans = 'clr', zero_replace = 'halfmin') %>% 
  ord_calc(method = 'PCA') %>% 
  ord_plot(
    alpha = 0.6, size = 2, color = 'birth_mode',
    plot_taxa = 1:6, tax_vec_length = 0.275,
    tax_lab_style = tax_lab_style(
      type = 'text', max_angle = 90, aspect_ratio = 0.7, 
      size = 3, fontface = 'bold'
    ),
  ) +
  theme_classic(12) +
  coord_fixed(0.7, clip = 'off')
pca

How to interpret the taxa loading vectors? Cautiously.

The relative length and direction of an arrow indicates how much that taxon contributes to the variation on each visible PC axis. e.g. Variation in Enterococcus contributes quite a lot to variation along the PC2 axis.

This allows you to infer that samples positioned at the bottom of the plot will tend to have higher relative abundance of Enterococcus than samples at the top of the plot.

Interestingly, samples on the right of the plot (which tend to be vaginally-delivered infants) seem to have relatively more Bifidobacterium, Bacteroides and Escherichia, whilst the C-section born infants have relatively more Veillonella.

There are caveats and nuance to the interpretation of these plots, which are called PCA bi-plots, and you can read more about here: TODO link to appropriate gusta me ecology page.

(Side note, Phocaeicola were considered part of Bacteroides until this year!)

Iris plot

You might have already noticed this pattern, when exploring and making barplots interactively with ord_explore earlier. We can make another kind of barplot now, using the PCA information to order our samples in a circular layout.

iris <- shao4 %>% 
  tax_filter(min_prevalence = 2.5/100, verbose = FALSE) %>% 
  tax_transform(rank = 'genus', trans = 'clr', zero_replace = 'halfmin') %>% 
  ord_calc(method = 'PCA') %>% 
  ord_plot_iris(
    tax_level = 'genus', n_taxa = 12, other = "Other", 
    anno_colour = 'birth_mode', anno_colour_style = list(alpha = 0.6, size = 0.6, show.legend = FALSE)
  )
iris

patchwork::wrap_plots(pca, iris, nrow = 1, guides = 'collect')

Taxon stats

From the PCA loadings and barplots above, we have some strong suspicions about which taxa have a higher relative abundance in vaginally delivered infants than in c-section delivered infants, and vice versa, but we can also statistically test this. This is often called “differential abundance” testing, in the style of “differential expression” testing from the transcriptomics field.

shao4 %>% 
  comp_barplot(
    tax_level = 'genus', n_taxa = 12, facet_by = 'birth_mode',
    label = NULL, bar_outline_colour = NA
  ) +
  coord_flip() +
  theme(axis.ticks.y = element_blank())

Model one taxon

We will start by creating a linear regression model for one genus, Bacteroides. We will transform the count data by first making it proportions, and then taking the binary logarithm, log2, after adding a pseudocount.

bacteroidesRegression1 <- shao4 %>% 
  tax_transform("compositional", rank = "genus") %>% 
  tax_transform("log2", zero_replace = "halfmin", chain = TRUE) %>%
  tax_model(type = "lm", rank = "genus", taxa = "Bacteroides", variables = "birth_mode") %>% 
  pluck(1) 
## Modelling: Bacteroides
# looking at the regression results
summary(bacteroidesRegression1)
## 
## Call:
## Bacteroides ~ birth_mode
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -7.7492 -0.6172 -0.6172  2.6421 18.0804 
## 
## Coefficients:
##                   Estimate Std. Error t value Pr(>|t|)    
## (Intercept)       -19.3756     0.4863  -39.84   <2e-16 ***
## birth_modevaginal   7.1320     0.6812   10.47   <2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 5.957 on 304 degrees of freedom
## Multiple R-squared:  0.265,  Adjusted R-squared:  0.2626 
## F-statistic: 109.6 on 1 and 304 DF,  p-value: < 2.2e-16
confint(bacteroidesRegression1)
##                        2.5 %     97.5 %
## (Intercept)       -20.332614 -18.418542
## birth_modevaginal   5.791662   8.472414
broom::tidy(bacteroidesRegression1, conf.int = TRUE)
## # A tibble: 2 × 7
##   term              estimate std.error statistic   p.value conf.low conf.high
##   <chr>                <dbl>     <dbl>     <dbl>     <dbl>    <dbl>     <dbl>
## 1 (Intercept)         -19.4      0.486     -39.8 1.08e-122   -20.3     -18.4 
## 2 birth_modevaginal     7.13     0.681      10.5 4.13e- 22     5.79      8.47

Click here for optional ggplot2 extension exercise:

Starting from a dataframe like the one produced by the code below, plot:

  1. Easy: The percentage prevalence of Bacteroides in each birth_mode group
  2. Medium: The distribution of relative abundance of Bacteroides in each birth_mode group, omitting zeros, on a log2 scale
  3. Hard: Do task 1 or 2 for for several taxa in one plot - (hint: pivot_longer)
  shao4 %>% 
  tax_transform("compositional", rank = "genus") %>% 
  ps_get() %>% 
  ps_otu2samdat(taxa = 'Bacteroides') %>% 
  samdat_tbl()

We can fit a model with covariates, as we did for PERMANOVA. We are going to convert the categorical variables into indicator (dummy) variables, and scale the continuous covariates to 0 mean and SD 1 (z-scores). You’ll see this will make our subsequent plots easier to interpret later.

shao4 <- shao4 %>% 
  ps_mutate(
    C_section = if_else(birth_mode == 'c_section', true = 1, false = 0),
    Female = if_else(sex == 'female', true = 1, false = 0),
    Birth_weight_Z = scale(birth_weight, center = TRUE, scale = TRUE),
    Reads_Z = scale(number_reads, center = TRUE, scale = TRUE)
  )
bacteroidesRegression2 <- shao4 %>% 
  tax_transform("compositional", rank = "genus") %>% 
  tax_transform("log2", zero_replace = "halfmin", chain = TRUE) %>%
  tax_model(
    type = "lm", rank = "genus", taxa = "Bacteroides", 
    variables = c('C_section', 'Female', 'Birth_weight_Z', 'Reads_Z')
  ) %>% 
  pluck(1) 
## Modelling: Bacteroides
# looking at the regression results
summary(bacteroidesRegression2)
## 
## Call:
## Bacteroides ~ C_section + Female + Birth_weight_Z + Reads_Z
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -9.4271 -2.1555 -0.4115  2.8176 18.1784 
## 
## Coefficients:
##                Estimate Std. Error t value Pr(>|t|)    
## (Intercept)    -11.7942     0.6103 -19.325   <2e-16 ***
## C_section       -7.5696     0.7206 -10.505   <2e-16 ***
## Female          -0.3809     0.7101  -0.536    0.592    
## Birth_weight_Z   0.3277     0.3514   0.932    0.352    
## Reads_Z          0.5361     0.3620   1.481    0.140    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 5.934 on 286 degrees of freedom
##   (15 observations deleted due to missingness)
## Multiple R-squared:  0.2854, Adjusted R-squared:  0.2754 
## F-statistic: 28.55 on 4 and 286 DF,  p-value: < 2.2e-16
broom::tidy(bacteroidesRegression2, conf.int = TRUE)
## # A tibble: 5 × 7
##   term           estimate std.error statistic  p.value conf.low conf.high
##   <chr>             <dbl>     <dbl>     <dbl>    <dbl>    <dbl>     <dbl>
## 1 (Intercept)     -11.8       0.610   -19.3   8.15e-54  -13.0      -10.6 
## 2 C_section        -7.57      0.721   -10.5   4.81e-22   -8.99      -6.15
## 3 Female           -0.381     0.710    -0.536 5.92e- 1   -1.78       1.02
## 4 Birth_weight_Z    0.328     0.351     0.932 3.52e- 1   -0.364      1.02
## 5 Reads_Z           0.536     0.362     1.48  1.40e- 1   -0.176      1.25

Many methods

This method is what MaAsLin2 does by default (except they call the compositional transformation “Total Sum Scaling (TSS)”). This is quite an uncomplicated method, so we will stick with this for today, but many statistical methods have been developed for differential abundance analyses.

Microbiome abundance data are quite awkward, statistically speaking, due to their sparseness and compositionality. Each successive method claims to handle some aspect of this awkwardness “better” than previous methods.

The aim is to have a method with adequate power to detect true associations, whilst controlling the type 1 error rate, the “false positive” reporting of associations that are not “truly” present.

Results are surprisingly inconsistent across the different methods, as demonstrated this year in a fascinating analysis by Jacob Nearing and colleagues.

What to do?

  • Filter out the noise & interpret results with caution! use multiple testing corrections
  • Remember it’s all relative (abundance)
  • Try multiple methods and/or use same method as previous study if replicating - Avoid Lefse and edgeR? - Beware: Not all methods allow covariate adjustment & few allow random effects (for time-series)

Model all the taxa!

We’re not normally interested in just one taxon! And often it’s also hard to decide which taxonomic rank we are most interested in modelling!

Lower ranks like species or ASVs give better resolution but also more sparsity and classification uncertainty… Higher ranks e.g. classes, could also be more powerful if you think most taxa within that class will follow a similar pattern.

Insert meme here?

So now we will fit a similar model for almost* every taxon at every rank we have available, from phylum down to species.

*We’ll actually filter out species with a prevalence of less than 10%.

# The code for `taxatree_models` is quite similar to tax_model. 
# However, you might need to run `tax_prepend_ranks` to ensure that each taxon at each rank is always unique. 
shaoModels <- shao4 %>% 
  tax_prepend_ranks() %>%
  tax_transform("compositional", rank = "species", keep_counts = TRUE) %>% 
  tax_filter(min_prevalence = 0.1, undetected = 0, use_counts = TRUE) %>% 
  tax_transform(trans = "log2", chain = TRUE, zero_replace = "halfmin") %>% 
  taxatree_models(
    type = lm, 
    ranks = c("phylum", "class", "order", "family", "genus", "species"), 
    variables = c('C_section', 'Female', 'Birth_weight_Z', 'Reads_Z')
  )
## Proportional min_prevalence given: 0.1 --> min 31/306 samples.
## 2022-05-24 06:10:21 - modelling at rank: phylum
## 2022-05-24 06:10:21 - modelling at rank: class
## 2022-05-24 06:10:21 - modelling at rank: order
## 2022-05-24 06:10:21 - modelling at rank: family
## 2022-05-24 06:10:21 - modelling at rank: genus
## 2022-05-24 06:10:21 - modelling at rank: species
shaoModels
## ps_extra object - a list with phyloseq and extras:
## 
## phyloseq-class experiment-level object
## otu_table()   OTU Table:         [ 39 taxa and 306 samples ]
## sample_data() Sample Data:       [ 306 samples by 15 sample variables ]
## tax_table()   Taxonomy Table:    [ 39 taxa by 6 taxonomic ranks ]
## 
## ps_extra info:
## tax_agg = species tax_transform = compositional&log2
## 
## $counts OTU Table: [ 39 taxa and 306 samples ]
## 
## $taxatree_models list:
## Ranks: phylum/class/order/family/genus/species

Why filter the taxa? It’s less likely that we are interested in rare taxa, and models of rare taxon abundances are more likely to be unreliable. Reducing the the number of taxa modelled also makes the process faster and makes visualizing the results easier!

Getting stats from the models

Next we will get a data.frame containing the regression coefficient estimates, test statistics and corresponding p values from all these regression models.

shaoStats <- taxatree_models2stats(shaoModels)
shaoStats
## ps_extra object - a list with phyloseq and extras:
## 
## phyloseq-class experiment-level object
## otu_table()   OTU Table:         [ 39 taxa and 306 samples ]
## sample_data() Sample Data:       [ 306 samples by 15 sample variables ]
## tax_table()   Taxonomy Table:    [ 39 taxa by 6 taxonomic ranks ]
## 
## ps_extra info:
## tax_agg = species tax_transform = compositional&log2
## 
## $counts OTU Table: [ 39 taxa and 306 samples ]
## 
## $taxatree_stats dataframe:
## 86 taxa at 6 ranks: phylum, class, order, family, genus, species 
## 4 terms: C_section, Female, Birth_weight_Z, Reads_Z
shaoStats$taxatree_stats
## # A tibble: 344 × 7
##    term           taxon             rank   estimate std.error statistic  p.value
##    <fct>          <chr>             <fct>     <dbl>     <dbl>     <dbl>    <dbl>
##  1 C_section      p: Proteobacteria phylum  -3.44       1.31    -2.62   9.23e- 3
##  2 Female         p: Proteobacteria phylum   0.595      1.29     0.460  6.46e- 1
##  3 Birth_weight_Z p: Proteobacteria phylum   0.0179     0.640    0.0279 9.78e- 1
##  4 Reads_Z        p: Proteobacteria phylum  -1.22       0.659   -1.86   6.44e- 2
##  5 C_section      p: Actinobacteria phylum -16.9        2.37    -7.12   8.98e-12
##  6 Female         p: Actinobacteria phylum  -2.80       2.33    -1.20   2.30e- 1
##  7 Birth_weight_Z p: Actinobacteria phylum   1.65       1.15     1.43   1.55e- 1
##  8 Reads_Z        p: Actinobacteria phylum  -2.86       1.19    -2.41   1.67e- 2
##  9 C_section      p: Firmicutes     phylum  15.3        4.21     3.62   3.43e- 4
## 10 Female         p: Firmicutes     phylum   0.741      4.15     0.179  8.58e- 1
## # … with 334 more rows

Adjusting p values

As we have performed a lot of statistical tests here, it is quite possible that could we find some significant p-values by chance alone.

So we should correct for multiple testing / control the false discovery rate or family-wise error rate.

Instead of applying these adjustment methods across all 86 taxa models at all ranks, the default behaviour is to control the family-wise error rate per taxonomic rank.

shaoStats <- shaoStats %>% taxatree_stats_p_adjust(method = "BH", grouping = "rank")
# notice the new variable
shaoStats$taxatree_stats
## # A tibble: 344 × 8
## # Groups:   rank [6]
##    term          taxon rank  estimate std.error statistic  p.value p.adj.BH.rank
##    <fct>         <chr> <fct>    <dbl>     <dbl>     <dbl>    <dbl>         <dbl>
##  1 C_section     p: P… phyl…  -3.44       1.31    -2.62   9.23e- 3      2.95e- 2
##  2 Female        p: P… phyl…   0.595      1.29     0.460  6.46e- 1      7.38e- 1
##  3 Birth_weight… p: P… phyl…   0.0179     0.640    0.0279 9.78e- 1      9.78e- 1
##  4 Reads_Z       p: P… phyl…  -1.22       0.659   -1.86   6.44e- 2      1.47e- 1
##  5 C_section     p: A… phyl… -16.9        2.37    -7.12   8.98e-12      7.19e-11
##  6 Female        p: A… phyl…  -2.80       2.33    -1.20   2.30e- 1      3.07e- 1
##  7 Birth_weight… p: A… phyl…   1.65       1.15     1.43   1.55e- 1      2.48e- 1
##  8 Reads_Z       p: A… phyl…  -2.86       1.19    -2.41   1.67e- 2      4.46e- 2
##  9 C_section     p: F… phyl…  15.3        4.21     3.62   3.43e- 4      1.83e- 3
## 10 Female        p: F… phyl…   0.741      4.15     0.179  8.58e- 1      9.16e- 1
## # … with 334 more rows

Plot all the taxatree_stats!

taxatree_plots() allows you to plot statistics (e.g. point estimates and significance) from all of the taxa models onto a tree layout. The taxon models are organised by rank, radiating out from the central root node from e.g. Phyla around the center to Species in the outermost ring.

taxatree_plots() itself returns a list of plots, which you can arrange into one figure with the patchwork package for example (and/or cowplot).

shaoStats %>% 
  taxatree_plots(node_size_range = c(1, 3), sig_stat = 'p.adj.BH.rank') %>% 
  patchwork::wrap_plots(ncol = 2, guides = "collect")

Taxatree Key

But how do we know which taxa are which nodes? We can create a labelled grey tree with taxatree_plotkey(). This labels only some of the taxa based on certain conditions that we specify.

set.seed(123) # label position 
key <- shaoStats %>% 
  taxatree_plotkey(
    taxon_renamer = function(x) stringr::str_remove(x, "[pfg]: "),
    # conditions below, for filtering taxa to be labelled
    rank == "phylum" | rank == "genus" & prevalence > 0.2
    # all phyla are labelled, and all genera with a prevalence of over 0.2
  ) 
key

You can do more with these trees to customise them to your liking. See an extended tutorial here on the microViz website: including how to directly label taxa on the colored plots, change the layout and style of the trees, and even how to use a different regression modelling approach.

Fin.

End workshop with recap and notes/pointers on current trends and topics not covered?

Session info

Records your package versions etc. Useful for debugging / reproducing analysis.

devtools::session_info()
## ─ Session info ───────────────────────────────────────────────────────────────
##  setting  value
##  version  R version 4.2.0 (2022-04-22)
##  os       macOS Monterey 12.4
##  system   aarch64, darwin20
##  ui       X11
##  language (EN)
##  collate  en_US.UTF-8
##  ctype    en_US.UTF-8
##  tz       Europe/Amsterdam
##  date     2022-05-24
##  pandoc   2.17.1.1 @ /Applications/RStudio.app/Contents/MacOS/quarto/bin/ (via rmarkdown)
## 
## ─ Packages ───────────────────────────────────────────────────────────────────
##  ! package          * version  date (UTC) lib source
##    ade4               1.7-19   2022-04-19 [2] CRAN (R 4.2.0)
##    ape                5.6-2    2022-03-02 [2] CRAN (R 4.2.0)
##    assertthat         0.2.1    2019-03-21 [2] CRAN (R 4.2.0)
##    backports          1.4.1    2021-12-13 [2] CRAN (R 4.2.0)
##    Biobase            2.56.0   2022-04-26 [2] Bioconductor
##    BiocGenerics       0.42.0   2022-04-26 [2] Bioconductor
##    biomformat         1.24.0   2022-04-26 [2] Bioconductor
##    Biostrings         2.64.0   2022-04-26 [2] Bioconductor
##    bitops             1.0-7    2021-04-24 [2] CRAN (R 4.2.0)
##    brio               1.1.3    2021-11-30 [2] CRAN (R 4.2.0)
##    broom              0.8.0    2022-04-13 [2] CRAN (R 4.2.0)
##    bslib              0.3.1    2021-10-06 [2] CRAN (R 4.2.0)
##    cachem             1.0.6    2021-08-19 [2] CRAN (R 4.2.0)
##    Cairo              1.5-15   2022-03-16 [2] CRAN (R 4.2.0)
##    callr              3.7.0    2021-04-20 [2] CRAN (R 4.2.0)
##    circlize           0.4.15   2022-05-10 [2] CRAN (R 4.2.0)
##    cli                3.3.0    2022-04-25 [2] CRAN (R 4.2.0)
##    clue               0.3-60   2021-10-11 [2] CRAN (R 4.2.0)
##    cluster            2.1.3    2022-03-28 [2] CRAN (R 4.2.0)
##    codetools          0.2-18   2020-11-04 [2] CRAN (R 4.2.0)
##    colorspace         2.0-3    2022-02-21 [2] CRAN (R 4.2.0)
##    ComplexHeatmap     2.12.0   2022-04-26 [2] Bioconductor
##    corncob            0.2.0    2021-03-11 [2] CRAN (R 4.2.0)
##    crayon             1.5.1    2022-03-26 [2] CRAN (R 4.2.0)
##    data.table         1.14.2   2021-09-27 [2] CRAN (R 4.2.0)
##    DBI                1.1.2    2021-12-20 [2] CRAN (R 4.2.0)
##    desc               1.4.1    2022-03-06 [2] CRAN (R 4.2.0)
##    devtools           2.4.3    2021-11-30 [2] CRAN (R 4.2.0)
##    digest             0.6.29   2021-12-01 [2] CRAN (R 4.2.0)
##    doParallel         1.0.17   2022-02-07 [2] CRAN (R 4.2.0)
##    dplyr            * 1.0.9    2022-04-28 [2] CRAN (R 4.2.0)
##    ellipsis           0.3.2    2021-04-29 [2] CRAN (R 4.2.0)
##    evaluate           0.15     2022-02-18 [2] CRAN (R 4.2.0)
##    fansi              1.0.3    2022-03-24 [2] CRAN (R 4.2.0)
##    farver             2.1.0    2021-02-28 [2] CRAN (R 4.2.0)
##    fastmap            1.1.0    2021-01-25 [2] CRAN (R 4.2.0)
##    forcats            0.5.1    2021-01-27 [2] CRAN (R 4.2.0)
##    foreach            1.5.2    2022-02-02 [2] CRAN (R 4.2.0)
##    fs                 1.5.2    2021-12-08 [2] CRAN (R 4.2.0)
##    future             1.25.0   2022-04-24 [2] CRAN (R 4.2.0)
##    future.apply       1.9.0    2022-04-25 [2] CRAN (R 4.2.0)
##    generics           0.1.2    2022-01-31 [2] CRAN (R 4.2.0)
##    GenomeInfoDb       1.32.1   2022-04-28 [2] Bioconductor
##    GenomeInfoDbData   1.2.8    2022-05-11 [2] Bioconductor
##    GetoptLong         1.0.5    2020-12-15 [2] CRAN (R 4.2.0)
##    ggforce            0.3.3    2021-03-05 [2] CRAN (R 4.2.0)
##    ggplot2          * 3.3.6    2022-05-03 [2] CRAN (R 4.2.0)
##    ggraph             2.0.5    2021-02-23 [2] CRAN (R 4.2.0)
##    ggrepel            0.9.1    2021-01-15 [2] CRAN (R 4.2.0)
##    GlobalOptions      0.1.2    2020-06-10 [2] CRAN (R 4.2.0)
##    globals            0.15.0   2022-05-09 [2] CRAN (R 4.2.0)
##    glue               1.6.2    2022-02-24 [2] CRAN (R 4.2.0)
##    graphlayouts       0.8.0    2022-01-03 [2] CRAN (R 4.2.0)
##    gridExtra          2.3      2017-09-09 [2] CRAN (R 4.2.0)
##    gtable             0.3.0    2019-03-25 [2] CRAN (R 4.2.0)
##    here               1.0.1    2020-12-13 [2] CRAN (R 4.2.0)
##    highr              0.9      2021-04-16 [2] CRAN (R 4.2.0)
##    htmltools          0.5.2    2021-08-25 [2] CRAN (R 4.2.0)
##    httpuv             1.6.5    2022-01-05 [2] CRAN (R 4.2.0)
##    igraph             1.3.1    2022-04-20 [2] CRAN (R 4.2.0)
##    IRanges            2.30.0   2022-04-26 [2] Bioconductor
##    iterators          1.0.14   2022-02-05 [2] CRAN (R 4.2.0)
##    jquerylib          0.1.4    2021-04-26 [2] CRAN (R 4.2.0)
##    jsonlite           1.8.0    2022-02-22 [2] CRAN (R 4.2.0)
##    knitr              1.39     2022-04-26 [2] CRAN (R 4.2.0)
##    labeling           0.4.2    2020-10-20 [2] CRAN (R 4.2.0)
##    later              1.3.0    2021-08-18 [2] CRAN (R 4.2.0)
##    lattice            0.20-45  2021-09-22 [2] CRAN (R 4.2.0)
##    lifecycle          1.0.1    2021-09-24 [2] CRAN (R 4.2.0)
##    listenv            0.8.0    2019-12-05 [2] CRAN (R 4.2.0)
##    magick             2.7.3    2021-08-18 [2] CRAN (R 4.2.0)
##    magrittr           2.0.3    2022-03-30 [2] CRAN (R 4.2.0)
##    MASS               7.3-57   2022-04-22 [2] CRAN (R 4.2.0)
##    Matrix             1.4-1    2022-03-23 [2] CRAN (R 4.2.0)
##    matrixStats        0.62.0   2022-04-19 [2] CRAN (R 4.2.0)
##    memoise            2.0.1    2021-11-26 [2] CRAN (R 4.2.0)
##    mgcv               1.8-40   2022-03-29 [2] CRAN (R 4.2.0)
##    microbiome         1.18.0   2022-04-26 [2] Bioconductor
##    microViz         * 0.9.1    2022-05-16 [1] Github (david-barnett/microViz@07f1fdb)
##    mime               0.12     2021-09-28 [2] CRAN (R 4.2.0)
##    multtest           2.52.0   2022-04-26 [2] Bioconductor
##    munsell            0.5.0    2018-06-12 [2] CRAN (R 4.2.0)
##    nlme               3.1-157  2022-03-25 [2] CRAN (R 4.2.0)
##    parallelly         1.31.1   2022-04-22 [2] CRAN (R 4.2.0)
##    patchwork          1.1.1    2020-12-17 [2] CRAN (R 4.2.0)
##    permute            0.9-7    2022-01-27 [2] CRAN (R 4.2.0)
##    phyloseq         * 1.40.0   2022-04-26 [2] Bioconductor
##    pillar             1.7.0    2022-02-01 [2] CRAN (R 4.2.0)
##    pkgbuild           1.3.1    2021-12-20 [2] CRAN (R 4.2.0)
##    pkgconfig          2.0.3    2019-09-22 [2] CRAN (R 4.2.0)
##    pkgload            1.2.4    2021-11-30 [2] CRAN (R 4.2.0)
##    plyr               1.8.7    2022-03-24 [2] CRAN (R 4.2.0)
##    png                0.1-7    2013-12-03 [2] CRAN (R 4.2.0)
##    polyclip           1.10-0   2019-03-14 [2] CRAN (R 4.2.0)
##    prettyunits        1.1.1    2020-01-24 [2] CRAN (R 4.2.0)
##    processx           3.5.3    2022-03-25 [2] CRAN (R 4.2.0)
##    promises           1.2.0.1  2021-02-11 [2] CRAN (R 4.2.0)
##    ps                 1.7.0    2022-04-23 [2] CRAN (R 4.2.0)
##    purrr            * 0.3.4    2020-04-17 [2] CRAN (R 4.2.0)
##    R6                 2.5.1    2021-08-19 [2] CRAN (R 4.2.0)
##    RColorBrewer       1.1-3    2022-04-03 [2] CRAN (R 4.2.0)
##    Rcpp               1.0.8.3  2022-03-17 [2] CRAN (R 4.2.0)
##    RCurl              1.98-1.6 2022-02-08 [2] CRAN (R 4.2.0)
##    registry           0.5-1    2019-03-05 [2] CRAN (R 4.2.0)
##  P remotes            2.4.2    2021-11-30 [?] CRAN (R 4.2.0)
##    reshape2           1.4.4    2020-04-09 [2] CRAN (R 4.2.0)
##    rhdf5              2.40.0   2022-04-26 [2] Bioconductor
##    rhdf5filters       1.8.0    2022-04-26 [2] Bioconductor
##    Rhdf5lib           1.18.0   2022-04-26 [2] Bioconductor
##    rjson              0.2.21   2022-01-09 [2] CRAN (R 4.2.0)
##    rlang              1.0.2    2022-03-04 [2] CRAN (R 4.2.0)
##    rmarkdown          2.14     2022-04-25 [2] CRAN (R 4.2.0)
##    rprojroot          2.0.3    2022-04-02 [2] CRAN (R 4.2.0)
##    rstudioapi         0.13     2020-11-12 [2] CRAN (R 4.2.0)
##    Rtsne              0.16     2022-04-17 [2] CRAN (R 4.2.0)
##    S4Vectors          0.34.0   2022-04-26 [2] Bioconductor
##    sass               0.4.1    2022-03-23 [2] CRAN (R 4.2.0)
##    scales             1.2.0    2022-04-13 [2] CRAN (R 4.2.0)
##    seriation        * 1.3.5    2022-03-28 [2] CRAN (R 4.2.0)
##    sessioninfo        1.2.2    2021-12-06 [2] CRAN (R 4.2.0)
##    shape              1.4.6    2021-05-19 [2] CRAN (R 4.2.0)
##    shiny            * 1.7.1    2021-10-02 [2] CRAN (R 4.2.0)
##    stringi            1.7.6    2021-11-29 [2] CRAN (R 4.2.0)
##    stringr            1.4.0    2019-02-10 [2] CRAN (R 4.2.0)
##    survival           3.3-1    2022-03-03 [2] CRAN (R 4.2.0)
##    testthat           3.1.4    2022-04-26 [2] CRAN (R 4.2.0)
##    tibble             3.1.7    2022-05-03 [2] CRAN (R 4.2.0)
##    tidygraph          1.2.1    2022-04-05 [2] CRAN (R 4.2.0)
##    tidyr              1.2.0    2022-02-01 [2] CRAN (R 4.2.0)
##    tidyselect         1.1.2    2022-02-21 [2] CRAN (R 4.2.0)
##    TSP                1.2-0    2022-02-21 [2] CRAN (R 4.2.0)
##    tweenr             1.0.2    2021-03-23 [2] CRAN (R 4.2.0)
##    usethis            2.1.5    2021-12-09 [2] CRAN (R 4.2.0)
##    utf8               1.2.2    2021-07-24 [2] CRAN (R 4.2.0)
##    vctrs              0.4.1    2022-04-13 [2] CRAN (R 4.2.0)
##    vegan              2.6-2    2022-04-17 [2] CRAN (R 4.2.0)
##    viridis            0.6.2    2021-10-13 [2] CRAN (R 4.2.0)
##    viridisLite        0.4.0    2021-04-13 [2] CRAN (R 4.2.0)
##    withr              2.5.0    2022-03-03 [2] CRAN (R 4.2.0)
##    xfun               0.31     2022-05-10 [2] CRAN (R 4.2.0)
##    xtable             1.8-4    2019-04-21 [2] CRAN (R 4.2.0)
##    XVector            0.36.0   2022-04-26 [2] Bioconductor
##    yaml               2.3.5    2022-02-21 [2] CRAN (R 4.2.0)
##    zlibbioc           1.42.0   2022-04-26 [2] Bioconductor
## 
##  [1] /Users/david/Documents/my_projects/evomics/evomics-material/renv/library/R-4.2/aarch64-apple-darwin20
##  [2] /Library/Frameworks/R.framework/Versions/4.2-arm64/Resources/library
## 
##  P ── Loaded and on-disk path mismatch.
## 
## ──────────────────────────────────────────────────────────────────────────────